import { ConfigPreview } from "@/docs/components/Preview";
import { Callout } from "nextra/components";

# External

Select data from a list, typically populated via a third-party API. Extends [Base](base).

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
      },
    },
    render: ({ data }) => {
      return <p style={{ margin: 0 }}>{data?.title || "No data selected"}</p>;
    },
  }}
/>

```tsx {5-15} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            // ... fetch data from a third party API, or other async source

            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
        },
      },
      render: ({ data }) => {
        return <p>{data?.title || "No data selected"}</p>;
      },
    },
  },
};
```

## Params

| Param                                     | Example                                      | Type       | Status   |
| ----------------------------------------- | -------------------------------------------- | ---------- | -------- |
| [`type`](#type)                           | `type: "external"`                           | "external" | Required |
| [`fetchList()`](#fetchlistqueryparams)    | `fetchList: async () => []`                  | Function   | Required |
| [`cache`](#cache)                         | `cache: { enabled: true }`                   | Object     | -        |
| [`filterFields`](#filterfields)           | `{ "rating": { type: "number" } }`           | Object     | -        |
| [`getItemSummary()`](#getitemsummaryitem) | `getItemSummary: async ({ title }) => title` | Function   | -        |
| [`initialFilters`](#initialfilters)       | `{ "rating": 1 }`                            | Object     | -        |
| [`initialQuery`](#initialquery)           | `initialQuery: "Hello, world"`               | String     | -        |
| [`mapProp()`](#mappropitem)               | `mapProp: async ({ title }) => title`        | Function   | -        |
| [`mapRow()`](#maprowitem)                 | `mapRow: async ({ title }) => title`         | Function   | -        |
| [`placeholder`](#placeholder)             | `placeholder: "Select content"`              | String     | -        |
| [`renderFooter()`](#renderfooterprops)    | `renderFooter: (props) => <p>Hello</p>`      | Function   | -        |
| [`showSearch`](#showsearch)               | `showSearch: true`                           | Boolean    | -        |

## Required params

### `type`

The type of the field. Must be `"external"` for Array fields.

```tsx {6} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
        },
      },
      // ...
    },
  },
};
```

### `fetchList(queryParams)`

Return a promise with a list of objects to be rendered in a tabular format via the external input modal.

The table will only render strings and numbers.

```tsx {7-14} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            // ... fetch data from a third party API, or other async source

            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
        },
      },
      // ...
    },
  },
};
```

#### `queryParams`

The parameters passed to the `fetchList` method based on your field configuration.

| Param                 | Example             | Type   |
| --------------------- | ------------------- | ------ |
| [`query`](#query)     | `"My Query"`        | String |
| [`filters`](#filters) | `"{ "rating": 1 }"` | Object |

##### `query`

The search query when using [`showSearch`](#showsearch).

##### `filters`

An object describing the filters configured by [`filterFields`](#filterfields).

## Optional params

### `cache`

Puck will automatically cache the output of the [`fetchList()`](#fetchlistqueryparams) function in memory.

You can disable this behaviour by setting `cache: { enabled: false }` on your field.

```tsx {7} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          cache: { enabled: true },
          fetchList: async () => {
            // ... fetch data from a third party API, or other async source

            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
        },
      },
      // ...
    },
  },
};
```

### `filterFields`

An object describing filters for your query using the [Fields API](/docs/api-reference/fields)

```tsx {13-17} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async ({ filters }) => {
            return [
              { title: "Apple", description: "Lorem ipsum 1", rating: 5 },
              { title: "Orange", description: "Lorem ipsum 2", rating: 3 },
            ].filter((item) => item.rating >= (filters.rating || 0));
          },
          filterFields: {
            rating: {
              type: "number",
            },
          },
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async ({ filters }) => {
          return [
            { title: "Apple", description: "Lorem ipsum 1", rating: 5 },
            { title: "Orange", description: "Lorem ipsum 2", rating: 3 },
          ].filter((item) =>
            item.rating >= (filters.rating || 0)
          )
        },
        filterFields: {
          rating: {
            type: "number",
          },
        },
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },

}}
/>

### `getItemSummary(item)`

Get the label to show once the item is selected. Returns a [ReactNode](https://react.dev/learn/typescript#typing-children).

<Callout type="warning">
  Avoid using interactive HTML elements like `<button>`, `<input>`, or `<a>` inside `getItemSummary`. These elements can interfere with the array field's built-in interactions (such as drag-and-drop reordering and item expansion). Stick to non-interactive elements like `<div>`, `<span>`, `<p>`, or plain text for summaries.
</Callout>

```tsx {13} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
          getItemSummary: (item) => item.title,
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
        getItemSummary: (item) => item.title,
      },
    },
    defaultProps: {
      data: {
        title: "Hello, world",
        description: "Lorem ipsum 1",
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },
  }}
/>

### `initialFilters`

The initial filter values when using [`filterFields`](#filterfields).

```tsx {18-20} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async ({ filters }) => {
            return [
              { title: "Apple", description: "Lorem ipsum 1", rating: 5 },
              { title: "Orange", description: "Lorem ipsum 2", rating: 3 },
            ].filter((item) => item.rating >= (filters.rating || 0));
          },
          filterFields: {
            rating: {
              type: "number",
            },
          },
          initialFilters: {
            rating: 1,
          },
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async ({ filters }) => {
          return [
            { title: "Apple", description: "Lorem ipsum 1", rating: 5 },
            { title: "Orange", description: "Lorem ipsum 2", rating: 3 },
          ].filter((item) =>
            item.rating >= (filters.rating || 0)
          )
        },
        filterFields: {
          rating: {
            type: "number",
          },
        },
        initialFilters: {
          rating: 1,
        },
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },

}}
/>

### `initialQuery`

Set an initial query when using showing a search input with [`showSearch`](#showsearch).

```tsx {16} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async ({ query }) => {
            return [
              { title: "Apple", description: "Lorem ipsum 1" },
              { title: "Orange", description: "Lorem ipsum 2" },
            ].filter((item) => {
              // ...
            });
          },
          showSearch: true,
          initialQuery: "Apple",
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async ({ query }) => {
          return [
            {
              title: "Apple",
              description:
                "An apple is a round, edible fruit produced by an apple tree.",
            },
            {
              title: "Orange",
              description:
                "An orange is a fruit of various citrus species in the family Rutaceae.",
            },
          ].filter((item) => {
            if (!query) return item;

            const queryLowercase = query.toLowerCase();

            if (item.title.toLowerCase().indexOf(queryLowercase) > -1) {
              return item;
            }

            if (item.description.toLowerCase().indexOf(queryLowercase) > -1) {
              return item;
            }
          })
        },
        showSearch: true,
        initialQuery: 'apple'
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },

}}
/>

### `mapProp(item)`

Modify the shape of the item selected by the user in the table before writing to the page data.

```tsx {13} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
          mapProp: (item) => item.description,
        },
      },
      render: ({ data }) => {
        return <p>{data || "No data selected"}</p>;
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
        mapProp: (item) => item.description,
      },
    },
    render: ({ data }) => {
      return <p>{data || "No data selected"}</p>;
    },
  }}
/>

### `mapRow(item)`

Modify the shape of the item before rendering it in the table. This will not affect the selected data.

```tsx {13} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
          mapRow: (item) => ({ ...item, title: item.title.toUpperCase() }),
        },
      },
      render: ({ data }) => {
        return <p>{data || "No data selected"}</p>;
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
        mapRow: (item) => ({ ...item, title: item.title.toUpperCase() }),
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },
  }}
/>

### `placeholder`

Set the placeholder text when no item is selected.

```tsx {13} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            return [
              { title: "Apple", description: "Lorem ipsum 1" },
              { title: "Orange", description: "Lorem ipsum 2" },
            ];
          },
          placeholder: "Pick your favorite fruit",
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async () => {
          return [
            {
              title: "Apple",
              description:
                "An apple is a round, edible fruit produced by an apple tree.",
            },
            {
              title: "Orange",
              description:
                "An orange is a fruit of various citrus species in the family Rutaceae.",
            },
          ];
        },
        placeholder: "Pick your favorite fruit",
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },
  }}
/>

### `renderFooter(props)`

Customize what will be displayed in the footer of the modal.

```tsx {13-15} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async () => {
            return [
              { title: "Hello, world", description: "Lorem ipsum 1" },
              { title: "Goodbye, world", description: "Lorem ipsum 2" },
            ];
          },
          renderFooter: ({ items }) => (
            <b>Custom footer with {items.length} results</b>
          ),
        },
      },
      render: ({ data }) => {
        return <p>{data || "No data selected"}</p>;
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async () => {
          return [
            { title: "Hello, world", description: "Lorem ipsum 1" },
            { title: "Goodbye, world", description: "Lorem ipsum 2" },
          ];
        },
        renderFooter: ({ items }) => (
          <b>Custom footer with {items.length} results</b>
        ),
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },
  }}
/>

### `showSearch`

Show a search input, the value of which will be passed to `fetchList` as the `query` param.

```tsx {15} copy
const config = {
  components: {
    Example: {
      fields: {
        data: {
          type: "external",
          fetchList: async ({ query }) => {
            return [
              { title: "Apple", description: "Lorem ipsum 1" },
              { title: "Orange", description: "Lorem ipsum 2" },
            ].filter((item) => {
              // ...
            });
          },
          showSearch: true,
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      data: {
        type: "external",
        fetchList: async ({ query }) => {
          return [
            {
              title: "Apple",
              description:
                "An apple is a round, edible fruit produced by an apple tree.",
            },
            {
              title: "Orange",
              description:
                "An orange is a fruit of various citrus species in the family Rutaceae.",
            },
          ].filter((item) => {
            if (!query) return item;

            const queryLowercase = query.toLowerCase();

            if (item.title.toLowerCase().indexOf(queryLowercase) > -1) {
              return item;
            }

            if (item.description.toLowerCase().indexOf(queryLowercase) > -1) {
              return item;
            }
          })
        },
        showSearch: true,
      },
    },
    render: ({ data }) => {
      return <p>{data?.title || "No data selected"}</p>;
    },

}}
/>

<div id="puck-portal-root" />
